iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 16
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 16

【Day.16】React入門 - 想要分頁? react-router-dom

  • 分享至 

  • xImage
  •  

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


本篇內容以react-router v5為主,react-router在v6後有大幅度改變,可參考官方文件或是下方邦友回覆
https://reactrouter.com/en/main/upgrading/v5

在過去,當我們要製作「分頁」時,多半是新增一個靜態HTML檔,讓web server根據檔案路徑去尋找,或是透過後端程式碼去定義什麼url要對應到哪個HTML檔。這種方式我們稱為伺服器渲染(SSR)

然而這卻也產生了一個問題。

即使頁面中大多是固定的Layout,但換頁的時候,因為是拜訪新檔案,整個頁面都要刷新。

為了解決這個問題,工程師決定也用Javascript從去創造前端路由控制器。換頁的時候,只用JS去改變不一樣的地方。這樣的網頁程式換頁時不需要整頁都刷新,使用起來跟APP很像,因此又稱為Single Page Application(SPA,單頁式網頁應用)。也因為大多統一成一個JS檔並改在瀏覽器製造頁面,這樣的方式也稱為客戶端渲染(CSR)。

React-router-dom就是在React達成前端路由的插件之一。他是基於React-router這個核心製作,衍伸的家族還有在react-native使用的react-router-native。

前置作業 - 製作分頁

這裡我們的Menu.jsMenuItem.js 會依照Day.12的程式,InputForm.js會依照Day.15的程式。然後我們要新增src/page資料夾,並在裡面製作兩個用來當作分頁的頁面:

  • src/page/MenuPage.js
import React from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

const MenuPage = () =>{
    let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);

    return <Menu title={"Andy Chang的like"}>{menuItemArr}</Menu>;
}

export default MenuPage;
  • src/page/FormPage.js
import React from 'react';
import InputForm from '../component/InputForm';

const FormPage = () =>{
    return <InputForm/>;
}

export default FormPage;

接下來,我們會嘗試在src/index.js來控制並創造控制分頁的路由。

環境設定

請打開terminal,並輸入

 npm i react-router-dom --save

安裝完畢後,進入src/index.js,在開頭引入

import React from 'react';
import ReactDOM from 'react-dom';


import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";

import {HashRouter,Route,Switch,Link} from "react-router-dom";

其中這一行會是所有我們要用到的元件

import {HashRouter,Route,Switch} from "react-router-dom";

HashRouter

路由器的英文是Router,但為甚麼這裡要加一個Hash呢? 這是因為如果我們要從前端去判斷當前的url是什麼,必須要在根路徑最後方加入一個#。JS才能從#後方的字串去判斷。

當然,React-router-dom也有提供不會有#BrowserRouter。但這個會需要後端的配合,我們目前只有純前端檔案就先用HashRouter。

現在,請在src/index.js創造一個元件App,讓React程式統一從這個元件渲染。並在裡面先加入<HashRouter></HashRouter>

import React from 'react';
import ReactDOM from 'react-dom';

import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";

const App = () =>{
    return( 
        <HashRouter>

        </HashRouter>
    );
}

ReactDOM.render(    
    <App/>,
    document.getElementById('root')
);

Switch

Switch這個元件是用來正確地判斷路由應該對應到誰。我們一樣在src/index.js裡面加入<Switch></Switch>

import React from 'react';
import ReactDOM from 'react-dom';

import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";

const App = () =>{
    return( 
        <HashRouter>
            <Switch>
            
            </Switch>
        </HashRouter>
    );
}

ReactDOM.render(    
    <App/>,
    document.getElementById('root')
);

Route

Route就是用來設定分頁的元件,用path這個props來設定url字串,它的使用方法有兩種:

  • 第一種方式,FormPage會被轉為React.creactElement
<Route path="/form" component={FormPage}/>
  • 第二種方式,用函式回傳React元件
<Route path="/form" render={()=>{return( <FormPage/> )}}/>

平常會用第一種方式,但如果你想要在分頁元件上綁props,就要用第二種。

現在,把我們的分頁元件用Route加進App中:

import React from 'react';
import ReactDOM from 'react-dom';

import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";

const App = () =>{
    return( 
        <HashRouter>
            <Switch>
                    <Route exact={true} path="/" component={MenuPage}/>
                    <Route path="/form" component={FormPage}/>
            </Switch>
        </HashRouter>
    );
}

ReactDOM.render(    
    <App/>,
    document.getElementById('root')
);

為什麼這裡<Route exact={true} path="/" component={MenuPage}/>要加上exact={true}呢? 這是因為path="/form"當中也有包含/。React router dom在檢查路由時是依照順序的,如果今天用戶拜訪了path="/form",React router dom會在檢查<Route path="/" component={MenuPage}/>時就認定包含/路由,所以顯示MenuPage

exact這個props就是用來限定路由一定要完全跟path一模一樣才顯示。因為是布林值,你也可以只寫名字不給值。

這樣就完成了分頁。

固定Layout

然而這樣並沒有顯示出SPA的感覺。所以現在我們來做一個固定的導覽列。請在src/index.js上方新增一個Layout元件:

import React from 'react';
import ReactDOM from 'react-dom';

import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";

const Layout = () => {
    return(

    )
}

const App = () =>{
/* 省略 */

然後在App中使用Layout把所有Route包起來:

const App = () =>{
    return( 
        <HashRouter>
            <Switch>
                <Layout>
                    <Route exact path="/" component={MenuPage}/>
                    <Route path="/form" component={FormPage}/>
                </Layout>
            </Switch>
        </HashRouter>
    );
}

然後我們就能在Layout中以props.children來顯示對應的Route

const Layout = (props) => {
    return(
        <>
            { props.children }
        </>
    )
}

但是目前我們還缺少了前往分頁的導覽列,請在Layout中新增<nav>

const Layout = (props) => {
    return(
        <>
            <nav>

            </nav> 
            { props.children }
        </>
    )
}

接下來就是要加入超連結了。

Link

一般講到超連結,我們會聯想到<a href="/路徑">,但這裡我們要使用的是React-router-dom提供的原件<Link>

為什麼要特別多弄一個元件呢?

這是因為<a href="/路徑">是預設導向主domain/路徑,當我們今天使用的是subdomain或是像hash router這種東西時,就要自己把subdomain或是#補進去,像是<a href="/#/路徑">。這樣當我們今天專案部屬環境不同時就很麻煩。

Link這個元件就會方便我們導向/統一管理要導向的路徑。它的語法是

<Link to="路徑">

現在,我們在開頭引入Link這個元素,並使用在<nav></nav>中。

import React from 'react';
import ReactDOM from 'react-dom';

import {HashRouter,Route,Switch,Link} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";

const Layout = (props) => {
    return(
        <>
            <nav>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/form" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
            </nav> 
            { props.children }
        </>
    )
}

所有的程式碼:

import React from 'react';
import ReactDOM from 'react-dom';

import {HashRouter,Route,Switch,Link} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";

const Layout = (props) => {
    return(
        <>
            <nav>
                <Link to="/">點我連到第一頁</Link>
                <Link to="/form" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
            </nav> 
            { props.children }
        </>
    )
}

const App = () =>{
    return( 
        <HashRouter>
            <Switch>
                <Layout>
                    <Route exact path="/" component={MenuPage}/>
                    <Route path="/form" component={FormPage}/>
                </Layout>
            </Switch>
        </HashRouter>
    );
}

ReactDOM.render(    
    <App/>,
    document.getElementById('root')
);

執行結果:

Link還能透過location api傳資料。詳請請參考官方文件

CSR的衍伸問題

CSR延伸的問題是因為程式碼都用JS處理,導致SEO的時候只會抓到原本那個空的<div id="root"></div>。為了解決這個問題,衍伸出了在後端製作React網頁(SSR)的方法。我們最後面會回頭來講這個


上一篇
【Day.15】React入門 - 非控制組件與useRef
下一篇
【Day.17】React入門 - 利用useContext進行多層component溝通
系列文
從比入門再往前一點開始,一直到深入React.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-04 15:19:03

Hi 最近這一兩周都在看你的文章,謝謝您的文章,作為入門的我來說真的很詳細很好懂!

我也有買你的書來看,不過目前有兩個回饋想跟您說一下:

  1. 書中的範例如果能提供github專案就好了,目前練習都要邊看書邊手工刻出來執行才知道結果。
  2. 關於現在這個章節,可能有些部份要標註一下RRD需要v6之前,v6之後改了很多東西,我知道的如下:

一、Switch由Routes取代,且Route中component關鍵字改成element

<Routes>
  <Route path='/' element={<h1>Home Page Component</h1>} />
  <Route path='/login' element={<h1>Login Page Component</h1>} />
    // New line
  <Route path='*' element={<Navigate to='/' />} />
</Routes>

二、不得在Routes/BrowserRoute中包含自訂Component或Layout
以前可以這樣:

<BrowserRouter>
    <Switch>
        <Layout>
            <Route exact path="/" component={FirstPage}/>
        </Layout>
    </Switch>
</BrowserRouter>

現在要改成這樣:

<BrowserRouter>
    <Routes>    
        <Route exact path="/" element={
            <Layout>
                <FirstPage />
            </Layout> 
        }/>
    </Routes>
</BrowserRouter>

三、this.props.match在DOM v6由useLocation、useParams取代
DOM v6之前:

const NotePage = ({match}) => {
	let noteId = match.params.id
}

DOM v6之後:
取得參數部分,由useParams取代,取得現在的URL,則變成是要用useLocation():

const NotePage = () => {
	let {id} = useParams();
    const location = useLocation();
    console.log(location.pathname);
}

四、Redirect Component在DOM v6中被Navigate Component取代
DOM v6之前:

<Switch>
    <Route exact path={`${this.props.match.path}`} component={Introd} />
    <Route path={`${this.props.match.path}/his`} component={His} />
                        //...
    <Redirect from={`${this.props.match.path}/story`} to={`${this.props.match.url}/his`} />
</Switch>

DOM v6之後:

<Routes>
  <Route path='/' element={<h1>Home Page Component</h1>} />
  <Route path='/login' element={<h1>Login Page Component</h1>} />
    // New line
  <Route path='*' element={<Navigate to='/' />} />
</Routes>

五、useHistory在DOM v6中被useNavigate取代
DOM v6之前:

function App() {
  const history = useHistory();
 
  const handleGoBack = () => {
    history.goBack();
  };
  return (
    <>
      <button onClick={handleGoBack}>Go Back</button>
    </>
  );
}

DOM v6之後:

import { useNavigate } from "react-router-dom";
import "./App.css";
 
function App() {
  const navigate = useNavigate();
 
  const handleGoBack = () => {
     navigate(-1); // new line
  };
  return (
    <>
      <button onClick={handleGoBack}>Go Back</button>
    </>
  );
}

目前書中還是舊版的方法,看看之後有沒有機會翻新內容,謝謝~

Andy Chang iT邦研究生 3 級 ‧ 2023-02-04 16:47:11 檢舉

Hello 感謝您的回饋跟肯定

我當初在寫書的時候本來是想說範例都跟這裡一樣,所以就沒有多寫一份到Github裡面。只是後來自己修改了一部份的內容,完書的時候才注意到這點,不過自己平常時間也不夠回頭修改每個部份了,不好意思真抱歉

也有個困難點是在前端技術迭代過快,我的鐵人賽文章分別建立在React 16.9和17.2去撰寫,但在出書後半年React 18又大改了一部份內容,React router v6也是在近一年更新的。如果要所有內容都跟上最新版本實在有點吃力(畢竟我在這邊有快70篇React相關內容......)。但我會盡量回覆這裡的問題,也鼓勵讀者以互動的方式讓這裡更像是社群討論的感覺,這樣也比較能用不同人的角度去理解盲點

我要留言

立即登入留言